from .utils    import CfList
from .variable import Variable

# ====================================================================
#
# VariableList object
#
# ====================================================================

class VariableList(CfList):
    '''

An ordered sequence of variables stored in a list-like object.

In some contexts, whether an object is a variable or a variable list
is not known and does not matter. So to avoid ungainly type testing,
some aspects of the VariableList interface are shared by a variable
and vice versa.

Any attribute or method belonging to a variable may be used on a
variable list and will be applied independently to each element.

Just as it is straight forward to iterate over the variables in a
variable list, a variable will behave like a single element variable
list in iterative and indexing contexts.

'''
    _reserved_attrs = Variable._reserved_attrs[:]

    def __delattr__(self, attr):
        '''
x.__delattr__(attr) <==> del x.attr

'''
        for v in self.__dict__['_list']:
            delattr(v, attr)
    #--- End: def

    def __getattribute__(self, attr, *default):
        '''
x.__getattribute__(attr) <==> x.attr

'''
        if (attr.startswith('__') or
            attr in super(VariableList, self).__getattribute__('__dict__') or
            attr in dir(self)):
            # Return an attribute of the list-like object
            return super(VariableList, self).__getattribute__(attr, *default)

        # Return a built-in list of the attributes from each element
        # of the cf list
        return [getattr(v, attr, *default)
                for v in super(VariableList, self).__getattribute__('_list')]
    #--- End: def

    def __setattr__(self, attr, value):
        '''
x.__setattr__(attr, value) <==> x.attr=value

'''
        if (attr.startswith('__') or
            attr in super(VariableList, self).__getattribute__('__dict__') or
            attr in dir(self)):
            # Set an attribute on the list-like object
            super(VariableList, self).__setattr__(attr, value)
            return
        #--- End: if

        # Still here? Then do a built-in setattr on each of the list's
        # elements
        for v in self.__dict__['_list']:
            setattr(v, attr, value)
    #--- End: def

    def __repr__(self):
        '''
x.__repr__() <==> repr(x)

'''
        return repr(self._list).replace('>, ', '>,\n ')
    #--- End: def

    def __str__(self):
        '''
x.__str__() <==> str(x)

'''
        return '\n\n'.join([v.__str__() for v in self._list])
    #--- End: def

    def __eq__(self, other):
        '''
x.__eq__(y) <==> x==y

'''
        return type(self)([v.__eq__(other) for v in self._list])
    #--- End: def

    def __ne__(self, other):
        '''
x.__ne__(y) <==> x!=y

'''
        return type(self)([v.__ne__(other) for v in self._list])
    #--- End: def

    def __neg__(self):
        '''
x.__neg__() <==> -x

'''
        return type(self)([v.__neg__() for v in self._list])
    #--- End: def

    def __pos__(self):
        '''
x.__pos__() <==> +x

'''
        return type(self)([v.__pos__() for v in self._list])
    #--- End: def

    def __abs__(self):
         '''
x.__abs__() <==> abs(x)

'''
         return type(self)([v.__abs__() for v in self._list])
    #--- End: def

    def __invert__(self):
         '''
x.__invert__() <==> ~x

'''
         return type(self)([v.__invert__() for v in self._list])
    #--- End: def

    def __add__(self, other):
        '''
x.__add__(y) <==> x+y

        '''
        return type(self)([v.__add__(other) for v in self._list])
    #--- End: def

    def __sub__(self, other):
        '''
x.__sub__(y) <==> x-y
        '''
        return type(self)([v.__sub__(other) for v in self._list])
    #--- End: def

    def __mul__(self, other):
        '''
x.__mul__(y) <==> x*y
        '''
        return type(self)([v.__mul__(other) for v in self._list])
    #--- End: def

    def __div__(self, other):
        '''
x.__div__(y) <==> x/y
'''
        return type(self)([v.__div__(other) for v in self._list])
    #--- End: def

    def __floordiv__(self, other):
        '''
x.__floordiv__(y) <==> x//y
'''
        return type(self)([v.__floordiv__(other) for v in self._list])
    #--- End: def


    def __truediv__(self, other):
        '''
x.__truediv__(y) <==> x/y
'''
        return type(self)([v.__truediv__(other) for v in self._list])
    #--- End: def

    def __radd__(self, other):
        '''
x.__radd__(y) <==> y+x
'''
        return type(self)([v.__radd__(other) for v in self._list])
    #--- End: def

    def __rmul__(self, other):
        '''
x.__rmul__(y) <==> y*x
'''
        return type(self)([v.__rmul__(other) for v in self._list])
    #--- End: def

    def __iadd__(self, other):
        '''
x.__iadd__(y) <==> x+=y
'''
        for v in self._list:
            v.__iadd__(other)
        return self
    #--- End def

    def __isub__(self, other):
        '''
x.__isub__(y) <==> x-=y
'''
        for v in self._list:
            v.__isub__(other)
        return self
    #--- End def

    def __imul__(self, other):
        '''
x.__imul__(y) <==> x*=y
'''
        for v in self._list:
            v.__imul__(other)
        return self
    #--- End def

    def __idiv__(self, other):
        '''
x.__idiv__(y) <==> x/=y
'''
        for v in self._list:
            v.__idiv__(other)
        return self
    #--- End def

    def __ifloordiv__(self, other):
        '''
x.__ifloordiv__(y) <==> x//=y
'''
        for v in self._list:
            v.__ifloordiv__(other)
        return self
    #--- End def

    def __itruediv__(self, other):
        '''
x.__itruediv__(y) <==> x/=y
'''
        for v in self._list:
            v.__itruediv__(other)
        return self
    #--- End def

    @property
    def subspace(self):
        '''

Subspace each variable in the list, returning a new list of variables.

**Examples**

>>> vl
[<CF Variable: air_temperature(73, 96)>,
 <CF Variable: air_temperature(73, 96)>]
>>> vl.subspace[0,0]
[<CF Variable: air_temperature(1,1)>,
 <CF Variable: air_temperature(1,1)>]

'''
        return SubspaceVariableList(self)
    #--- End: def

    def dump(self, *arg, **kwargs):
        '''

Return a string containing the full descriptions of each variable in
the list.

'''
        return '\n\n\n'.join([v.dump(*arg, **kwargs) for v in self._list])
    #--- End: def

    def delprop(self, prop):
        '''

Delete a CF property from each element of the list.

:Parameters:

    prop : str
        The name of the property to delete.

:Returns:

    None

**Examples**

>>> len(fl)
2
>>> fl.getprop('foo')
['bar', 'bar']
>>> fl.delprop('foo')
>>> fl.getprop('foo', 5)
[5, 5]

Note that recognised CF propeties (such as 'long_name') may be
deleted directly:

>>> del fl.standard_name
>>> getattr(fl, 'standard_name', None)
[None, None]

'''
        for v in self._list:
            v.delprop(prop)
    #--- End: def
    
    def set_equals(self, other, rtol=None, atol=None, traceback=False):
        '''

True if two instances are set-wise equal, False otherwise.

Two instances are set-wise equal if their attributes are equal and
their elements are equal set-wise (i.e. the order of the lists is
irrelevant).

:Parameters:

    other : 
        The object to compare for equality.

    atol : float, optional
        The absolute tolerance for all numerical comparisons, By
        default the value returned by the `ATOL` function is used.

    rtol : float, optional
        The relative tolerance for all numerical comparisons, By
        default the value returned by the `RTOL` function is used.

    traceback : bool, optional
        If True then print a traceback highlighting where the two
        instances differ.

:Returns: 

    out : bool
        Whether or not the two instances are equal.

**Examples**


'''
        if len(self) != len(other):
            if traceback:
                print("%s: Different numbers of variables" %
                      self.__class__.__name__)
            return False
        #--- End: if
        
        sd = {}
        od = {}
        
        for vl, d in zip((self, other), (sd, od)):
            for x in vl:
                identity = x.identity()
                if identity in d:
                    d.append(x)
                else:
                    d[identity] = [x]
        #--- End: def

        if set(sd) != set(od):
            return False
        
        for identity in sd.iterkeys():            
            if len(sd[identity]) != len(od[identity]):
                if traceback:
                    print("%s: Different numbers of '%s' variables" %
                          (self.__class__.__name__, identity))
                return False
            #--- End: if
            
            match = False
            for x in sd[identity]:
                for i, y in enumerate(od[identity]):
                    if x.equals(y, rtol=rtol, atol=atol, traceback=False):
                        match = True
                        break
                #--- End: for
                if match:
                    od[identity].remove(i)
                else:
                    if traceback:                        
                        print("%s: Different variable: %s" %
                              (self.__class__.__name__, repr(x)))
                    return False
        #--- End: for

        return True
    #--- End: def

    def getprop(self, prop, *default):
        '''

Return a built-in list of a CF properties from each element.

:Parameters:

    prop : str
        The name of the property to get.

    default : optional
        Return `default` for each element that does not have the named
        property.

:Returns:

    out : list
        The value of the named property, or the default value, for
        each element.

:Raises:

    AttributeError :
        If any element does not have the named property and a default
        value has not been set.

**Examples**

>>> len(fl)
2
>>> fl.getprop('foo')
['bar', 'bar']
>>> fl.delprop('foo')
>>> fl.getprop('foo', 0.0)
[0.0, 0.0]

Note that recognised CF propeties (such as 'long_name') may be
retrieved directly:

>>> fl.standard_name
['air_temperature', 'air_temperature']
>>> getattr(fl, 'long_name', 'default')
['default', 'default']

'''
        return [v.getprop(prop, *default) for v in self._list]
    #--- End: def

    def hasprop(self, prop):
        '''

Return a built-in list describing whether each element has a CF
property.

:Parameters:

    prop : str
        The name of the property to get.

:Returns:

    out : list or bools
        Whether each element has the named property.        

**Examples**

>>> len(fl)
2
>>> fl.hasprop('foo')
[False, True]

**Warning:** Recognised CF propeties (such as 'long_name') may be
interrogated with the built-in hasattr, but with a different result:

>>> getattr(fl, 'standard_name', None)
['air_tmperature', None]
>>> hasattr(fl, 'standard_name')
True
>>> fl.hasprop('standard_name')
[True, False]

'''
        return [v.hasprop(prop) for v in self._list]
    #--- End: def

    def match(self, *args, **kwargs):
        '''

Return a list of booleans showing which elements match the given
conditions.

The match conditions are passed to each element's `match` method in
turn.

:Parameters:

    args, kwargs :
        As for the variable's `match` method.

:Returns:

    out : list
        A built-in list of booleans showing which elements match the
        conditions.

**Examples**

>>> fl
[<>
 <>]
>>> fl.match(attr={'standard_name': 'air_temperature'})
[True, False]

'''
        return [v.match(*args, **kwargs) for v in self._list]
    #--- End: def

    def name(self, *arg, **kwargs):
        '''

Return a built-in list of the names of each element of the list of
variables.

'''
        return [v.name(*arg, **kwargs) for v in self._list]
    #--- End: def

    def setprop(self, prop, value):
        '''

Set a CF property on each element of the list.

:Parameters:

    prop : str
        The name of the property to set.

    value :
        The value for the property.

:Returns:

    None

**Examples**

>>> len(fl)
2
>>> fl.setprop('foo', 'bar')
>>> fl.getprop('foo')
['bar', 'bar']

Note that recognised CF propeties (such as 'long_name') may be set
directly:

>>> fl.standard_name = 'air_temperature'
>>> fl.standard_name
['air_temperature', 'air_temperature']

'''
        for v in self._list:
            v.setprop(prop, value)
    #--- End: def

    def sort(self, *args, **kwargs):
        '''

Sort the elements in place.

The sort is stable which means that when multiple records have the
same key, their original order is preserved.

:Parameters:

    args, kwargs :
        As for the built-in list's sort method.

:Returns:

    None

**Examples**

Two ways of sorting by standard name:

>>> fl.sort(key=lambda f: f.standard_name)
>>> from operator import attrgetter
>>> fl.sort(key=attrgetter('standard_name'))

Get the elements in reverse standard name order:

>>> fl.sort(key=attrgetter('standard_name'), reverse=True)

Sort by standard name then by the value of the first element of the
data array:

>>> fl.sort(key=attrgetter('standard_name', 'first_datum'))

Sort by standard name then by the value of the second element of the
data array:

>>> fl.sort(key=lambda f: (f.standard_name, f.subspace[..., 1].first_datum))

'''
        self._list.sort(*args, **kwargs)
    #--- End: def

    def subset(self, *args, **kwargs):
        '''

Return the elements which match the given conditions.

The match conditions are passed to each element's `match` method in
turn.

:Parameters:

    args, kwargs :
        As for the variable's `match` method.

:Returns:

    out :
        A new list containing the matching elements.

**Examples**

>>> fl
[<>
 <>]
>>> fl.subset(attr={'standard_name': 'air_temperature'})
[<>]

'''
        return type(self)([v
                           for v in self._list
                           if v.match(*args, **kwargs)]
                          )
    #--- End: def

    def override_units(self, *args, **kwargs):
        '''

Override the each element's data array units in place.

Not to be confused with setting the Units attribute to units which are
equivalent to each element's original units.

This is different to setting the Units attribute, as the new units
need not be equivalent to the original ones and the each data array
will not be changed to reflect the new units.

'''
        for v in self._list:
            v.override_Units(*args, **kwargs)
    #--- End: def

#--- End: class


# ====================================================================
#
# SubspaceVariableList object
#
# ====================================================================

class SubspaceVariableList(object):
    '''

'''
    __slots__ = ['variablelist']

    def __init__(self, variablelist):
        '''

Set the contained list of variables.

'''
        self.variablelist = variablelist
    #--- End: def

    def __getitem__(self, indices):
        '''
x.__getitem__(indices) <==> x[indices]

'''
        variablelist = self.variablelist
        return type(variablelist)([v.subspace[indices] for v in variablelist])
    #--- End: def

    def __setitem__(self, index, value):
        '''
x.__setitem__(index, value) <==> x[index]=value

'''
        for v in self.variablelist:
            v.subspace[index] = value
    #--- End: def

#--- End: class
